module("nginx_operator", package.seeall)

local ox = require('posix_c')
local utility = require('gray_utils')

local sleep = ngx.sleep
local popen = utility.popen
local set_non_blocking = utility.set_non_blocking
local is_file_exist = utility.is_file_exist
local print_and_log = utility.print_and_log

local BUFSIZE = utility.BUFSIZE

local ngx_bin_name = ngx.var.nginx_binary_name
local ngx_pid_name = ngx.var.nginx_pid_name
local ngx_conf_name = ngx.var.nginx_conf_name
local ngx_conf_prefix = ngx.var.nginx_conf_prefix

local ecode_success = utility.ecode_success
local ecode_warn = utility.ecode_warn
local ecode_fatal_error = utility.ecode_fatal_error
local ecode_env_exception = utility.ecode_env_exception
local ecode_retry_after_modify = utility.ecode_retry_after_modify
local ecode_retry_later = utility.ecode_retry_later


function check_nginx_status(pid)
    local ngx_pid_oldbin_fn = ngx_pid_name .. ".oldbin"
    if is_file_exist(ngx_pid_oldbin_fn) then
        return "upgrading"
    end
    if not pid then
        local f, m = io.open(ngx_pid_name, "r")
        if f == nil then
            -- TODO get from ps aux?
            if string.match(m, "No such file") then
                return "stopped"
            else
                return nil, m 
            end
        end
        pid = f:read("*all")
        f:close()
        if #pid == 0 then return "stopped" end
        pid = tonumber(pid)
    end
    -- might be a zombie process
    -- ox.wait(pid, ox.WNOHANG)
    -- to check if pid exist(TODO check ESRCH)
    local r, em, en  = ox.kill(pid, 0)
    if not r then
        if en == ox.ESRCH then return "stopped" end
        -- return nil, em -- maybe "permission denied"
        return "running", pid
    else
        return "running", pid
    end
end


function operate_nginx(action, data)
    local function test_nginx(ngx_conf_name)
        local accept_warn
        if ngx.var.arg_accept_warn == "true" then accept_warn = true end
        local cmd = ngx_bin_name.." -t -c "..ngx_conf_name
        local stderr_r, pid = popen(cmd, nil, true)
        if stderr_r == nil then return ecode_env_exception, pid end
        set_non_blocking(stderr_r)
        local stderr_msg = ""
        local need_sleep = true
        local try_times = 150
        while true do
            if need_sleep then
                try_times = try_times - 1
                if try_times <= 0 then
                    ox.close(stderr_r)
                    return ecode_env_exception, "test nginx maybe blocked!"
                end
                sleep(0.1)
            end
            local r, em, en = ox.read(stderr_r, BUFSIZE)
            if not r then
                if en == ox.EINTR then
                    need_sleep = false
                elseif en == ox.EAGAIN then
                    need_sleep = true
                else
                    ox.close(stderr_r)
                    return ecode_env_exception, em
                end
            else
                if #r == 0 then break end
                stderr_msg = stderr_msg..r
                need_sleep = true
            end
        end
        ox.close(stderr_r)
        if string.match(stderr_msg, "%[error%]")
            or string.match(stderr_msg, "%[crit%]")
            or string.match(stderr_msg, "%[alert%]")
            or string.match(stderr_msg, "%[emerg%]") then
            return ecode_retry_after_modify, stderr_msg
        end
        local conf_ok = 0
        if string.match(stderr_msg, "syntax is ok") then
            conf_ok = conf_ok + 1
        end
        if string.match(stderr_msg, "test is successful") then
            conf_ok = conf_ok + 1
        end
        if conf_ok < 2 then
            return ecode_retry_after_modify, stderr_msg
        else
            local rc = ecode_warn
            if select(2, string.gsub(stderr_msg, "\n", "\n")) == 2 then
                stderr_msg = nil
                rc = ecode_success
            elseif not accept_warn then
                rc = ecode_retry_after_modify
            end
            return rc, stderr_msg
        end
    end

    local function stop_nginx(pid, force)
        local signal = ox.SIGQUIT
        if force then signal = ox.SIGTERM end
        local r, em, en = ox.kill(pid, signal)
        if not r then
            if en == ox.SRCH then return ecode_success end
            return ecode_env_exception, em
        end
        local try_times = 100
        while true do
            sleep(0.1)
            local r, m = check_nginx_status()
            if not r then return ecode_env_exception, m end
            if r == "stopped" then
                return ecode_success
            end
            try_times = try_times - 1
            if try_times <= 0 then return ecode_retry_later, "stop timeout" end
        end
    end

    local function start_nginx()
        local cmd = ngx_bin_name.." -c "..ngx_conf_name
        local stderr_r, pid = popen(cmd, nil, true)
        if stderr_r == nil then return ecode_env_exception, pid end
        -- ox.close(stderr_r)
        set_non_blocking(stderr_r)
        local stderr_msg = ""
        local failed, emsg
        -- TODO get errmsg from stderr, wait_pid(pid, O_NONBLOCK)
        local try_times = 100
        while true do
            sleep(0.1)
            -- read error msg
            local rr, em, en = ox.read(stderr_r, BUFSIZE)
            local r, m = check_nginx_status()
            if not r then failed = ecode_env_exception; emsg = m; break end
            if r == "running" then
                failed = ecode_success; emsg = m; break
            end
            if not rr then
                if en ~= ox.EINTR and en ~= ox.EAGAIN then
                    failed = ecode_env_exception; emsg = em; break
                end
            else
                if #rr == 0 then
                    if string.match(stderr_msg, "%[error%]")
                       or string.match(stderr_msg, "%[crit%]")
                       or string.match(stderr_msg, "%[alert%]")
                       or string.match(stderr_msg, "%[emerg%]") then
                        failed = ecode_retry_after_modify
                        emsg = stderr_msg
                        break
                    end
                else
                    stderr_msg = stderr_msg..rr
                end
            end
            try_times = try_times - 1
            if try_times <= 0 then
                failed = ecode_env_exception
                emsg = "start nginx timeout! nginx may blocked by something!"
                break
            end
        end
        ox.close(stderr_r)
        if failed ~= ecode_success then
            stop_nginx(pid, true)
        end
        return failed, emsg
    end

    local function reload_nginx(pid)
        local signal = ox.SIGHUP
        local r, em = ox.kill(pid, signal)
        if not r then
            return ecode_env_exception, em
        end
        sleep(0.1)
        return ecode_success
    end

    local function restart_nginx(pid, force)
        -- shutdown now
        stop_nginx(pid, "force")
        return start_nginx()
    end


    if action == "start" then
        return start_nginx()
    elseif action == "stop" then
        return stop_nginx(data)
    elseif action == "stop_f" then
        return stop_nginx(data, "force")
    elseif action == "restart" then
        return restart_nginx(data)
    elseif action == "restart_f" then
        return restart_nginx(data, "force")
    elseif action == "reload" then
        return reload_nginx(data)
    elseif action == "test" then
        return test_nginx(data)
    else
        return ecode_success
    end
end

getmetatable(nginx_operator).__newindex = function (table, key, val)
    error('Attempt to write to undeclared variable "' .. key .. '": '
          .. debug.traceback())
end

